/**
 * Copyright 2012 ibiblio
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0.txt
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.ibiblio.terasaur.bt

import org.ibiblio.terasaur.item.Item
import org.springframework.web.multipart.MultipartFile
import org.springframework.web.multipart.MultipartHttpServletRequest
import org.ibiblio.terasaur.bencode.BencodeException
import org.ibiblio.terasaur.bt.TorrentController
import org.ibiblio.terasaur.search.DateHelper

// TODO check use of these
import java.io.ByteArrayInputStream
import java.io.InputStream

/**
 * Add torrent to item.  Process uploaded file, perform lots of checks,
 * parse torrent file, add Torrent, associate with given item id, then
 * redirect to appropriate item page.
 *
 * *done Upload torrent file to tmp space
 *
 * TODO: Check valid login session
 * *done Check for valid item id input param
 *
 * *done Bdecode torrent
 * *done handle error for invalid bencoded data
 * *done Get info hash from torrent metadata
 * *done Check for duplicate info hash
 * *done Check announce urls in the torrent file against DH app whitelist
 *
 * *done Get item record for item id
 * *done Handle error for item not found
 * TODO: Check user's authorization to edit document
 *
 * *done Determine torrent save path from info hash
 * *done Check that destination file doesn't already exist
 * *done Save torrent file to destination path
 *
 * *done Extract torrent file size from file upload
 * *done Determine torrent file name (use temp name from upload)
 * *done Extract torrent name
 *
 * *done Torrent published value defaults to value of containing item record
 * *done Save torrent domain object:
 *     * info hash
 *     * torrent file size
 *     * torrent file name
 *     * torrent data size
 *     * created
 *     * modified
 *     * published
 * *done Catch save error and delete torrent file
 * *done If success:
 *     * add "Torrent added" flash message
 *     * redirect to item edit page
 *
 */
class TorrentServiceAddCommand {
    private _service
    private _request
    private _item
    private _bytes

    TorrentServiceAddCommand(Map params) {
        _service = params.service
        _request = params.request
        _item = params.item
        _bytes = params.bytes
    }

    public Map run() {
        Map result
        if (_request) {
            result = _add_byRequest(_item, _request)
        } else {
            result = _add_byByteArray(_item, _bytes)
        }
        return result
    }

    protected Map _add_byRequest(Item item, MultipartHttpServletRequest request) {
        Map uploadData = _getUploadFileData(request)
        ByteArrayInputStream bytes = uploadData.bytes
        return _add_byByteArray(item, uploadData.bytes)
    }

    protected Map _getUploadFileData(MultipartHttpServletRequest request) {
        MultipartFile upload = (MultipartFile)request.getFile(TorrentController._torrentUploadName)
        if (!upload) {
            throw new TorrentServiceException('Missing or invalid uploaded file')
        }
        if (upload.isEmpty()) {
            throw new TorrentServiceException('The uploaded file was empty')
        }

        return [
            bytes: new ByteArrayInputStream(upload.getBytes()),
            filename: upload.getOriginalFilename()
            ]
    }

    protected Map _add_byByteArray(Item item, ByteArrayInputStream bytes) {
        if (!item) {
            throw new TorrentServiceException('Missing or invalid item')
        }
        TorrentFileManager fm = _service._getTorrentFileManager()
        TorrentMetainfo metainfo = fm.getTorrentMetainfoForInputStream(bytes, true)
        bytes.reset()
        _checkDuplicateInfoHash(metainfo)
        _validateTrackerList(metainfo)
        ByteArrayInputStream newBytes = _appendTerasaurIdentifier(metainfo, item, bytes)

        Map saveParams = [
            'item': item,
            'metainfo': metainfo,
            'bytes': newBytes
            ]
        String message = _saveTorrent(saveParams)

        return [
            itemId: item.id,
            message: message
            ]
    }

    protected void _checkDuplicateInfoHash(TorrentMetainfo info) {
        Torrent t = Torrent.get(info.info_hash)
        if (t) {
            throw new TorrentServiceException('Torrent ' + t.name + ' (' + t.infoHash + ') already exists')
        }
    }

    /**
     * If not already present, append the terasaur item identifier url to
     * the torrent comment.
     *
     * @param metainfo
     * @return
     */
    private ByteArrayInputStream _appendTerasaurIdentifier(TorrentMetainfo metainfo, Item item, ByteArrayInputStream oldBytes) {
        ByteArrayInputStream newBytes = null
        // canonicalLink returns empty string if item is null
        def canonicalLink = _service.urlHelper.canonicalLink(item, 'show')

        if (canonicalLink) {
            canonicalLink = _service.urlHelper.serverUrl() + canonicalLink
            boolean reencode = false
            if (!metainfo.comment) {
                metainfo.comment = canonicalLink
                reencode = true
            } else if (!metainfo.comment.contains(canonicalLink)) {
                metainfo.comment = metainfo.comment + "\n\n" + canonicalLink
                reencode = true
            }
            if (reencode) {
                newBytes = _reencodeTorrent(metainfo, item)
            }
        }

        if (!newBytes) {
            newBytes = oldBytes
        }
        return newBytes
    }

    protected ByteArrayInputStream _reencodeTorrent(TorrentMetainfo metainfo, Item item) {
        ByteArrayInputStream newBytes = null
        def bytes = metainfo.encode()
        newBytes = new ByteArrayInputStream(bytes.toByteArray())

        // Sanity check to make sure the info hash didn't change
        TorrentFileManager fm = _service._getTorrentFileManager()
        def newMetainfo = fm.getTorrentMetainfoForInputStream(new BufferedInputStream(newBytes), true)
        newBytes.reset()

        if (newMetainfo.info_hash != metainfo.info_hash) {
            _service.log.error('info_hash mismatch after torrent comment change (item id: ' + item.id + ', old: ' + metainfo.info_hash + ', new: ' + newMetainfo.info_hash + ')')
            throw new TorrentServiceException('There was an unexpected problem saving this torrent.')
        }
        return newBytes
    }

    protected void _validateTrackerList(TorrentMetainfo info) {
        ArrayList<String> approvedTrackers = _service.grailsApplication.config.terasaur.torrent.approved_trackers

        if (!approvedTrackers || approvedTrackers.isEmpty()) {
            throw new TorrentServiceException('Approved tracker list is not configured')
        }

        boolean found = false
        ArrayList<String> trackers = info.getAnnounceList()
        Iterator<String> iter = trackers.iterator()
        String tracker
        while (iter.hasNext()) {
            tracker = iter.next()
            if (approvedTrackers.indexOf(tracker) > -1) {
                found = true
                break
            }
        }
        if (!found) {
            throw new TorrentServiceException('Torrent does not have an approved tracker (' + trackers.join(',') + ')')
        }
    }

    /**
     * Param keys:
     *     itemId: Long
     *	   metainfo: TorrentMetainfo
     *	   bytes: ByteArrayInputStream
     *
     * @param Map
     * @return
     */
    protected String _saveTorrent(Map params) {
        TorrentFileManager fm = _service._getTorrentFileManager()
        String message
        try {
            fm.save(params.bytes)
            Torrent.withTransaction { status ->
                _saveTorrent_transaction(params.metainfo, params.item, params.bytes.getBytes().length)
            }
            // Update search index
            _service.searchService.add(params.item)
            message = "Torrent added (" + params.metainfo.info.name + ")"
        } catch (BencodeException e) {
            // Most likely caused by stream read error
            message = "Bencode error saving torrent"
            _service.log.error(message, e)
        } catch (Exception e) {
            // If save fails, remove torrent file
            _service.log.error("Save torrent error", e)

            // In the case of an info hash on disk but not in the database, we should
            // not roll back the file save operation and delete the torrent file.
            // That would be bad.
            if (! ("java.io.IOException".equals(e.class.name) && e.message.startsWith("File exists:")) ) {
                fm.remove(params.metainfo.info_hash)
            }

            message = e.message
        }
        return message
    }

    protected void _saveTorrent_transaction(TorrentMetainfo metainfo, Item item, int filesize) {
        Torrent t = new Torrent(
            infoHash: metainfo.info_hash,
            name: metainfo.info.name,
            size: metainfo.size,
            filename: metainfo.info.name + TorrentFileManager.TORRENT_FILE_EXTENSION,
            filesize: filesize,
            published: DateHelper.now(),
            item: item
            )

        if (!t.save(insert:true, flush:_service._flushTransactions)) {
            String field = "infoHash"
            if (t.errors.hasFieldErrors(field)) {
                def errors = t.errors.getFieldErrors(field)
                def uniqueError = false
                errors.each {
                    if (it.code == "unique") {
                        uniqueError = true
                    }
                    _service.log.error('Torrent save field error: ' + it.field + ' (' + it.code + ')')
                }
                if (uniqueError) {
                    throw new TorrentServiceException("A torrent with this info hash already exists (" + metainfo.info_hash + ")")
                }
            }
            throw new TorrentServiceException("Torrent save error")
        }
    }
}
